<?php

/**
 * Компонент для создания форм.
 *
 */
abstract class Kernel_Action_Form extends Kernel_Action_Abstract 
{
    
    /**
     * Метод подачи запроса "GET".
     *
     */
    const GET = 'get';

    /**
     * Метод подачи запроса "POST".
     *
     */
    const POST = 'post';

    /**
     * Флаг формирования формы по таблице, включая поля с указанными наименованиями.
     *
     */
    const INCLUSIVE = 1;

    /**
     * Флаг формирования формы по таблице, исключая поля с указанными наименованиями.
     *
     */
    const EXCLUSIVE = 2;

    /**
     * Идентификатор элемента, редактируемого в форме.
     *
     * @var mixed
     */
    public $id;

    /**
     * Как устаканится, опишу.
     *
     * @var array
     */
    public $elements = array();

    /**
     * URL для перенаправления агента после успешного сохранения формы.
     *
     * :KLUDGE: morozov 17102007: Вроде как это не совсем дело формы — перенаправлять,
     * это больше похоже на задачу для менеджера списков или кого-нибудь подобного
     * на фронтенде. Поживем — увидим :)
     *
     * @var string
     */
    public $redirect_url;

    /**
     * Хранилище данных формы. Если указано, то форма загружается и сохраняется
     * через него.
     *
     * @var Kernel_Form_Storage
     */
    public $storage;

    /**
     * Path to the template file.
     *
     * @var string
     */
    protected $_template = 'backend/component/form.tpl';

    /**
     * Путь к файлу шаблона страницы с сообщением о том, что форма сохранена.
     * Если не указан, форма просто будет перерисована.
     *
     * @var string
     */
    protected $_template_saved;
    
    /**
    * User success message. 
    * 
    * @param string
    */
    protected $_message_success; 
    
    /**
    * User error message. 
    * 
    * @param string
    */
    protected $_message_error; 

    /**
     * Метод, по которому браузер должен отправить данные формы.
     *
     * @var string
     */
    protected $_method = self::POST;

    /**
     * Значение атрибута "enctype" формы.
     *
     * @var string
     */
    protected $_enctype;

    /**
     * Значение атрибута "action" формы.
     *
     * @var string
     */
    protected $_action = '';

    /**
     * Флаг, указывающий, что в форме может только редактироваться существующая
     * запись, но не создаваться новая. С использованием этого флага я буду
     * делать формы, которые доступны только для зарегистрированных пользователей
     * либо для пользователей с определенными ролями.
     *
     * @var boolean
     */
    protected $_update_only = false;

    /**
     * Наименование поля - уникального идентификатора записи в таблице.
     *
     * @var string
     */
    protected $_id_field = 'id';

    /**
     * Флаг, указывающий, что после сохранения данных формы не нужно показывать
     * сответствующее сообщение.
     *
     * @var boolean
     */
    protected $_no_message = false;
    
    /**
    * @desc Flag pointing out that after form submit page will self-redirect
    * 
    * @param boolean
    */
    protected $_self_redirect = true; 

    /**
     * Элементы формы
     *
     * @var array
     */
    private $_elements = array();

    /**
     * Массив исходных значений формы. Может использоваться при сохранении для
     * определения, было ли измененено значение элемента. FALSE означает, что
     * значения еще не были загружены (в большинстве случаев они просто не нужны).
     *
     * @var array
     */
    private $_initial_values = false;

    /**
     * Общая ошибка формы (которую нельзя отнести к какому-то определенному полю).
     *
     * @var string
     */
    private $_error;

    /**
     * Ошибки элементов формы.
     *
     * @var array
     */
    private $_errors = array();

    /**
     * Является ли элемент формы вновь создаваемым.
     *
     * @var boolean
     */
    private $_is_new;

    /**
     * Конструктор.
     *
     * :NOTE: morozov: 09042008: если нужно перекрывать конструктор и вычислять
     * параметры в нем, то конструктор предка (т.е. этот) нужно вызывать _после_
     * вычисления параметров
     *
     * @param   array   $params
     */
    public function __construct($params = array()) 
    {
        // разбираем переданные параметры
        parent::__construct($params);
        
        // выставляем флаг перед инициализацией входных параметров, 
        $this->_is_new = !$this->id;
        
        // инициализируем элементы фотмы
        $this->_initElements();
        
        // получаем дополнительные параметры элементов из описания компонента
        foreach ($this->elements as $name => $params) {
            $this->_setElementParam($name, $params);
        }
        // выставляем значения полей-файлов как объекты
        foreach ($this->_elements as $name => $params) {
            if ('file' == $params['type'] && is_null($params['value'])) {
                $this->_setElementParam($name, 'value', new Kernel_Form_Value_File_Blank());
            }
        }
    }
    
    public function process()
    {
        // set template_saved id it exists
        $template_saved = Kernel_Form_Template::get();
        $this->_template = $template_saved ? $template_saved : $this->_template;
        
        parent::process();
    }
    
    /**
     * Возвращает путь к шаблону формы.
     *
     * @return  string
     */
    /*final public function getTemplate() 
    {
        $template_saved = Kernel_Form_Template::get();
        return $template_saved ? $template_saved : $this->_template;
    }*/

    /**
     * Возвращает набор данный формы, которые нужно передать в шаблон.
     *
     * @return  array
     */
    public function getTemplateData() 
    {
        return array(
            'method' => $this->_method,
            'action' => $this->_action 
                ? $this->_action 
                : Application::getRequest()->url(),
            'enctype' => is_null($this->_enctype)
                ? ($this->_getFileElements()
                    ? 'multipart/form-data'
                    : 'application/x-www-form-urlencoded')
                : $this->_enctype,
            'required' => (boolean)$this->_getElementsByFilter(array('required' => true)),
            'elements' => $this->_tmpGetElements(),
            'errors' => $this->_getErrors(),
            'new' => $this->_isNew(), 
        );
    }

    /**
     * Загружает данные по умолчани. Должно использоваться для загрузки данных
     * формы в составе многостраничной формы.
     *
     * @return  array
     */
    public function load() 
    {
        return $this->_load();
    }

    /**
     * Сохраняет указанные данные. Должно использоваться для сохранения данных
     * формы в составе многостраничной формы.
     *
     */
    public function save($values) 
    {
        $this->_setValue($values);
        $this->_save();
    }

    /**
     * Возвращает, разрешено ли использование фильтра текущим пользователем -
     * только в том случае, если форме разрешено создавать записи либо запись
     * обновляется.
     *
     * @return  boolean
     */
    /*protected function _allowed() 
    {
        return parent::_allowed() && !($this->_update_only && $this->_isNew());
    }*/

    /**
     * Реализация обработки пользовательского запроса.
     *
     */
    protected function _process() 
    {
        // загружаем исходные данные формы в любом случае, т.к. надо поднять
        // значения readonly полей
        $this->_tmpLoad();
        
        if ($this->_isSubmitted())
        {
            $this->_submitted();
        }
        
        $this->_draw();
    }

    /**
     * Вызывается для обработки отправленных данных.
     *
     * :TODO: morozov 19032008: неплохо бы переобозвать с указанием, что же
     * именно этот метод делает :)
     */
    protected function _submitted() 
    {
        if (self::POST == $this->_method && !$this->_isRefererValid())
        {
            $this->_deny('HTTP_REFERER does not match HTTP_HOST');
        }

        foreach (Kernel_Form_Upload::get() as $name => $value) {
            if ($this->_elementExists($name)) {
                $this->_elements[$name]['value'] = $value;
            }
        }

        $this->_errors = array();

        // проверяем не побились ли данные POST
        // @link http://bugs.php.net/bug.php?id=31775
        if (self::POST == $this->_method && $this->_isPostFailed()) {
            $this->_error(__('UPLOAD_ERR_POST_SIZE'), ini_get_bytes('post_max_size'));
        } else {
            $this->_getValuesFromRequest();
            $this->_filter();
            $this->_validate();
        }

        try {
            if (!$this->_isValid()) {
                $message = null === $this->_error
                    ? ($this->_message_error ? 
                        $this->_message_error : 
                        Kernel_Response_Message::compose(get_class($this), 'ERROR'))
                    : $this->_error;
                throw new Kernel_Form_Exception_Validate($message);
            }
            
            Kernel_Filter_Captcha::clear();
            
            if (!is_null($this->storage)) {
                Kernel_Form_Upload::store($this->_getFiles());
                Kernel_Form_Upload::temporize();
                $this->storage->save($this->_getValues());
            } else {
                $this->_save();
            }
            
            if (!$this->_no_message && is_null($this->storage)) {
                if (is_null($this->_message_success)) {
                    $this->_message_success = array(get_class($this), $this->_isNew() ? 'ADDED' : 'SAVED');
                }
                Application::getResponse()->message(true, $this->_message_success);
            }

            if (is_null($this->storage)) {
                Kernel_Form_Upload::clear();
            }

            $this->_done();

        } catch (Kernel_Form_Exception_Process $e) {
            Application::getResponse()->message(false, $e->getMessage());
            Kernel_Form_Upload::store($this->_getFiles());
        }
    }

    /**
     * Возвращает представление формы для текущего ответа.
     *
     * @return  Kernel_Form_View_Abstract
     */
    private function _view() 
    {
        static $view;
        $response = Application::getResponse();
        switch (true) {
            case isset($view):
                return $view;
            case $response instanceof Kernel_Response_Html:
                $view = new Kernel_Form_View_Html;
                break;
            case $response instanceof Kernel_Response_Json:
                $view = new Kernel_Form_View_Json;
                break;
            default:
                trigger_error('Unable to…', E_USER_ERROR);
                break;
        }
        return $view;
    }

    protected function _tmpLoad() 
    {
        /*if (is_null($this->storage)) {
            Kernel_Form_Upload::clear();
        }*/
        $loaded = false;

        switch (true) {
            case !is_null($this->storage):
                $values = $this->storage->load();
                $loaded = true;
                break;
            case !$this->_isNew():
                $values = $this->_load();
                $loaded = true;
                break;
        }

        if ($loaded)
        {
            if (empty($values))
            {
                $this->_abort('Record not found');
            }

            $this->_setValue($values);
        }
    }

    protected function _done() 
    {
        // пока так, потом приберемся
        if (is_null($this->storage))
        {
            // if exists redirect url... 
            if (!is_null($this->redirect_url)) {
                $this->_location(str_replace('%25id', $this->id, $this->redirect_url));
            }
            // .. and self redirect if post form is submitted and it's allowed 
            elseif (self::POST == $this->_method && $this->_self_redirect) {
                // in case of template_saved store it
                if ($this->_template_saved) {
                    Kernel::factory('Kernel_Form', 'Template', $this->_template_saved)
                        ->store();
                }
                $this->_location(Application::getRequest()->url());
            }
            
            // else nothing to do !?
            
        } else {
            $this->storage->done();
        }
    }

    /**
     * @desc Return whether form is a new one. 
     *
     * @return  boolean
     */
    protected function _isNew() 
    {
        return $this->_is_new;
    }

    /**
     * Выводит данные формы в шаблон.
     *
     * @deprecated  переделать Security_Action_Forgot и убрать в _process()
     */
    protected function _draw() 
    {
        $this->_view()->display($this);
    }

    /**
     * Возвращает сообщения об ошибках формы.
     *
     * @return array
     */
    protected function _getErrors() 
    {
        return $this->_errors;
    }

    /**
     * Подменяет шаблон страницы на шаблон сохраненной формы.
     *
     */
    /*protected function _replaceTemplate() 
    {
        Application::getResponse()->setTemplate($this->_template_saved);
    }*/

    private function _getFileElements() 
    {
        return $this->_getElementsByFilter(array('type' => 'file'));
    }

    /**
     * Определяет, была ли отправлена форма.
     *
     * @param   string  $name
     * @return  mixed
     */
    protected function _isSubmitted()
    {
        switch ($this->_method)
        {
            case self::POST:
                return 'POST' == $_SERVER['REQUEST_METHOD'];
                break;

            case self::GET:
                foreach (array_keys($this->_elements) as $param)
                {
                    if (isset($_GET[$param]))
                    {
                        return true;
                    }
                }
                return false;
                break;
        }

        return false;
    }

    /**
     * Возвращает текущее значение поля формы с указанным именем и индексом,
     * если это массив полей.
     *
     * @param   string  $name
     * @param   mixed   $index
     * @return  mixed
     */
    protected function _getValue($name, $index = null)
    {
        return is_null($index)
            ? $this->_elements[$name]['value']
            : $this->_elements[$name . '[' . $index . ']']['value'];
    }

    /**
     * Возвращает текущее значение поля формы с указанным именем и индексом,
     * если это массив полей.
     *
     * @param   string  $name
     * @param   mixed   $index
     * @return  mixed
     */
    protected function _getInitialValue($name, $index = null)
    {
        if (false === $this->_initial_values)
        {
            $this->_initial_values = $this->_load();
        }
        
        return is_null($index)
            ? $this->_initial_values[$name]
            : $this->_initial_values[$name . '[' . $index . ']'];
    }

    /**
     * Возвращает изменилось ли значение указанного поля формы,
     *
     * @param   string  $name
     * @param   mixed   $index
     * @return  boolean
     */
    protected function _valueChanged($name, $index = null) 
    {
        return $this->_getValue($name, $index) != $this->_getInitialValue($name, $index);
    }

    /**
     * Возвращает текущие значения полей формы.
     *
     * @return  array
     */
    protected function _getValues()
    {
        $values = array();

        foreach ($this->_elements as $name => $params)
        {
            eval('$values' . $this->_getElementIndex($name) . ' = $params["value"];');
        }
        
        return $values;
    }

    /**
     * Устанавливает значение поля формы.
     *
     * @param   mixed   $name
     * @param   mixed   $value
     */
    protected function _setValue($name, $value = null)
    {
        $values = is_array($name) ? $name : array($name => $value);

        foreach (array_keys($this->_elements) as $name)
        {
            $index = $this->_getElementIndex($name);

            // :KLUDGE: morozov 15012008: заменить на array_key_exists
            eval('if (isset($values' . $index . ')) $this->_elements[$name]["value"] = $values' . $index . ';');
        }
    }

    /**
     * Загружает данные формы.
     *
     * @return  array
     */
    protected function _load() 
    {
        return array();
    }

    /**
     * Сохраняет данные формы.
     *
     * @throws  Kernel_Form_Exception_Save
     */
    protected function _save() 
    {
        
        foreach ($this->_getFileElements() as $element) {
            $file = $element['value'];
            if ($file->successful() && isset($element['save']) && !$file->existed()) {
                call_user_func($element['save'], $file);
            }
        }
        
        // еже ли форма не новая, либо установлен флаг "только апдейт"
        if (!$this->_isNew() || $this->_update_only) {
            $this->_update();
        } else {
            $this->_insert();
        }
    }

    /**
     * Добавляет новую запись.
     *
     */
    protected function _insert()
    {
        trigger_error(get_class($this) . '::_insert() is not implemented', E_USER_ERROR);
    }

    /**
     * Обновляет существующую запись.
     *
     */
    protected function _update()
    {
        trigger_error(get_class($this) . '::_update() is not implemented', E_USER_ERROR);
    }

    /**
    * Initialize form elements.
    * 
    */
    abstract protected function _initElements();
    
    /**
    * Filter values before validation
    * 
    */
    protected function _filter() 
    {
        foreach ($this->_elements as &$element) {
            foreach ($element['filters'] as $definition) {
                $filter = Kernel_Form_Element_Filter_Factory::create($definition);
                $element['value'] = $filter->filter($element['value'], $this->_getValues());
            }
        }
    }

    /**
     * Выполняет проверку данных формы.
     *
     */
    protected function _validate()
    {
        foreach (array_keys($this->_elements) as $name)
        {
            $this->_validateElement($name);
        }

        $this->_validatePasswords();
    }

    /**
     * Возвращает элемент формы с указанным именем.
     *
     * @param   string  $name
     * @return  array
     */
    protected function _getElement($name)
    {
        return $this->_elements[$name];
    }

    /**
     * Возвращает значение параметра элемента формы с указанным именем.
     *
     * @param   string  $name
     * @param   string  $param
     * @return  mixed
     */
    protected function _getElementParam($name, $param)
    {
        return array_key_exists($param, $this->_elements[$name]) 
            ? $this->_elements[$name][$param] 
            : false;
    }

    /**
     * Возвращает, является ли указанный элемент обязательным для заполнения.
     * Может быть перекрыт для реализации поведения, когда правила проверки
     * значений одного элемента зависят от "внешних условий", например,
     * значения другого элемента.
     *
     * @param   string  $name
     * @return  boolean
     */
    protected function _isRequired($name) {
        return $this->_getElementParam($name, 'required');
    }

    /**
     * Выполняет проверку элемента формы формы.
     *
     * @param   string  $name
     */
    protected function _validateElement($name) 
    {
        // если значение поля не указано
        if ($this->_isBlank($this->_getValue($name))) {
            // и при этом оно обязательно для заполнения
            if (false !== ($required = $this->_isRequired($name))) {
                // выводим общее сообщение об ошибке
                $this->_error($name, __(is_string($required) ? $required : null));
            }
        } else {
            // выполняем проверку значения поля
            $this->_validateValue($name);
        }
    }

    /**
     * Проверяет совпадение друг с другом значений полей типа "password".
     * Надеюсь, переделывать не придется.
     *
     * @param   string  $element1[, string  $element2, …]
     */
    protected function _validatePasswords()
    {
        $only = func_get_args();

        $elements = array();

        // собираем в кучу все элементы типа "password"
        foreach ($this->_elements as $name => $params)
        {
            if ('password' == $params['type'] && (empty($only) || in_array($name, $only)))
            {
                $value = $this->_getValue($name);

                // если хотя бы одно поле не заполнено, и оно обязательно,
                // опускаем эту проверку, т.к. ошибка вылезет на предыдущем
                // этапе проверки
                if ($params['required'] && utf8_strlen($value) == 0)
                {
                    return;
                }
                else
                {
                    $elements[$name] = $this->_getElement($name);
                }
            }
        }

        // если элементов набралось больше одного, т.е. есть что с чем сравнивать
        if (count($elements) > 1)
        {
            // берем значение первого элемента за эталон
            $initial = array_shift($elements);

            // сравниваем с ним значения всех остальных
            foreach ($elements as $name => $params)
            {
                if ($params['value'] != $initial['value'])
                {
                    $this->_error($name, __('VALIDATION_ERROR_PASSWORD_MISMATCH'));
                }
            }
        }
    }

    /**
     * Проверяет значение указанного поля (с учетом того, что оно непустое).
     *
     * @param   string $name
     * @return  boolean
     */
    protected function _validateValue($name)
    {
        $params = $this->_getElement($name);

        // собираем валидаторы элемента. это:
        $validators = array_merge
        (
            // валидаторы, "навязываемые" типом данных элемента
            $this->_getTypeValidators($params),

            // и те, которые указаны явно
            (array)$params['validators']
        );

        // формируем значение поля для проверки. если поле не содержит набор
        //  значений (что встречается в подавляющем большинстве случаев)
        $values = !$this->_tmpIsMultiple($params)
            // оборачиваем его в массив одного элемента - для единообразия
            ? array($params['value'])
            : $params['value'];
        //var_dump($values);
        foreach ($values as $value) {
            try {
                $this->_chain($validators, $value);
            } catch (Kernel_Form_Exception_Element $e) {
                $this->_error($name, $e->getMessage());
                return false;
            }
        }

        return true;
    }

    /**
     * Проверяет значение с помощью цепочки валидаторов.
     *
     * @param   array   $validators
     * @param   mixed   $value
     * @throws  Kernel_Form_Exception_Element
     */
    protected function _chain($validators, $value) 
    {
        foreach ($validators as $definition) {
            $validator = Kernel_Validator_Factory::create($definition);
            if (!$validator->isValid($value)) {
                throw new Kernel_Form_Exception_Element($validator->getMessage());
            }
        }
    }

    /**
     * Возвращает определение валидатора значения указанного типа.
     *
     * @param   array   $params
     * @return  mixed
     */
    protected function _getTypeValidators($params) 
    {
        switch ($params['type']) {
            case 'integer':
                $validator = 'integer';
                break;

            case 'float':
                $validator = 'number';
                break;

            case 'email':
                $validator = 'email';
                break;

            case 'date':
                $validator = 'date';
                break;

            case 'list':
                $validator = array('in-array', array('values' => array_keys_recursive($params['values'])));
                break;

            default:
                $validator = null;
                break;
        }
        return is_null($validator) ? array() : array($validator);
    }

    /**
     * Добавляет элемент формы.
     *
     * @param   string  $name
     * @param   mixed   $type
     * @param   array   $params
     */
    protected function _addElement($name, $type = null, $params = array())
    {
        if ($this->_elementExists($name))
        {
            throw new Exception('Element "' . $name . '" already exists');
        }

        // это все пойдет в дефолтовые значения свойств класса
        $blank = array
        (
            'type' => 'string',
            'caption' => __(utf8_uppercase($name)),
            'note' => null,
            'value' => null,
            'required' => false,
            'filters' => array(),
            'validators' => array(),
            'readonly' => false,
            'display' => true,
        );

        $params += empty($type)
            ? array()
            : (is_array($type)
                ? $type
                : array('type' => $type));

        $params = array_merge($blank, $params, (array)@$this->elements[$name]);
        
        // в случае ручной валидации, дабы смарти не ругался на отсутствие параметра
        if ('list' == $params['type'] && !isset($params['values'])) {
            $params['values'] = array();
        }
        
        // :KLUDGE: morozov 18012008:
        if ('list' == $params['type'] && !empty($params['filter']) && count($params['values']) <= 1)
        {
            $value = key($params['values']);
            unset($params['type'], $params['values']);
            $this->_addElement($name, array_merge($params, array(
                'display' => false,
                'value' => $value,
            )));
            return;
        }

        // :KLUDGE: morozov 07092007:
        if ('date' == $params['type'])
        {
            $params = array_merge(array(
                'value' => date('Y-m-d'),
                'format' => __('DATE_FORMAT_CALENDAR'),
                'start_year' => null,
                'end_year' => null,
            ), $params);
        }
        $this->_elements[$name] = $params;
        
    }

    /**
     * Устанавливает значение параметра(ов) элемента формы.
     *
     * @param   string  $name
     * @param   mixed   $param
     * @param   mixed   $value
     */
    protected function _setElementParam($name, $param, $value = null) 
    {
        if (is_array($name)) {
            if (is_numeric_array($name)) {
                foreach ($name as $n) {
                    $this->_setElementParam($n, $param, $value);
                }
            } else {
                foreach ($name as $n => $p) {
                    $this->_setElementParam($n, $p);
                }
            }
        } elseif ($this->_elementExists($name)) {
            $this->_elements[$name] = array_merge($this->_elements[$name], is_array($param) 
                ? $param : array($param => $value));
        } else {
            trigger_error('There is no element named "' . $name . '" whithin form', E_USER_ERROR);
        }
    }

    /**
     * Устанавливает положение элемента в списке.
     *
     * @param   string  $name
     * @param   mixed   $position
     */
    protected function _setElementPosition($name, $position) 
    {
        switch (true) {
            case is_string($position):
                $position = array_search($position, array_keys($this->_elements)) + 1;
                break;
            case null === $position:
                $position = count($this->_elements) - 1;
                break;
            case $position < 0:
                $position = count($this->_elements) + $position - 1;
                break;
        }
        array_set_element_position($this->_elements, $name, $position);
    }

    /**
     * Устанавливает значение параметра "required" указанных элементов формы
     * в TRUE. Просто я пока не додумался, как по структуре таблицы для
     * текстовых полей догадаться, обязательные они или нет. Никак наверное.
     *
     * @param   string  $name [, string …]
     */
    protected function _setAsRequired() 
    {
        $elements = func_num_args() > 0
            ? array_values_recursive(func_get_args())
            : array_keys($this->_elements);

        foreach ($elements as $name) {
            $this->_setElementParam($name, 'required', true);
        }
    }

    /**
     * Регистрирует сообщение об ошибке, возникшее при проверке данных формы.
     *
     * @param   string $name
     * @param   string $message
     */
    protected function _error($name, $message = null) 
    {
        if (func_num_args() > 1) {
            // ошибка указанного элемента
            $this->_errors[$name] = $message;
        } else {
            // общая ошибка формы
            $this->_error = $name;
        }
    }

    /**
     * Возвращает, существует ли элемент с указанным наименованием. Может быть
     * использован в формах с динамическим набором полей.
     *
     * @param   string  $name
     * @param   mixed   $index
     * @return  boolean
     */
    protected function _elementExists($name, $index = null) 
    {
        if (!is_null($index)) {
            $name .= '[' . $index . ']';
        }
        return array_key_exists($name, $this->_elements);
    }

    protected function _fromTable($table, $names = array(), $flag = null) 
    {
        $elements = self::INCLUSIVE == $flag
            ? Kernel_Form_FromTable::inclusive($table, $names)
            : Kernel_Form_FromTable::exclusive($table, 
                // include id field in any case (e.g. if its name is not "id")
                'id' == $this->_id_field ? array_merge(array($this->_id_field), $names) : $names
                );
        
        foreach ($elements as $name => $params)
        {
            $this->_addElement($name, $params);
        }

        return array_keys($elements);
    }

    /**
     * Возвращает элементы формы, определение которых попадает под указанный
     * фильтр.
     *
     * @param   array   $filter
     * @return  array
     */
    protected function _getElementsByFilter($filter)
    {
        return array_filter($this->_elements, create_function('$value', '$filter = ' . var_export($filter, true) . '; return array_intersect_assoc($value, $filter) == $filter;'));
    }

    /**
     * Возвращает, является ли поле незаполненным. Потом разбросаем по классам
     * полей.
     *
     * @param   string  $value
     * @return  boolean
     */
    protected function _isBlank($value) 
    {
        switch (true) {
            // значения-объекты (пока реализовано только для файлов)
            case $value instanceof Kernel_Form_Value_Interface:
                return $value->isBlank();
            // значения-массивы
            case is_array($value):
                return !count($value);
            // остальные типы рассматриваем как строки
            default:
                return !utf8_strlen($value);
        }
    }

    /**
     * Возвращает, прошла ли успешно последняя проверка формы.
     *
     * Перевел из private в protected для того, чтобы в ходе проверки можно было
     * отслеживать результаты прошедших шагов, плюс, в сложных формах может быть
     * введен дополнительный контейнер ошибок.
     *
     * @return  boolean
     */
    protected function _isValid() 
    {
        // нет ошибок в элементах формы, и нет общей ошибки
        return empty($this->_errors) && is_null($this->_error);
    }

    /**
     * Возвращает данные пользовательского запроса в зависимости от метода формы.
     *
     * @return  array
     */
    private function _getRequestData()
    {
        switch ($this->_method)
        {
            case self::POST:
                $result = $_POST;
                break;

            case self::GET:
                $result = $_GET;
                break;

            default:
                $result = null;
                break;
        }

        return $result;
    }

    /**
     * Возвращает данные для сохранения. По умолчанию возвращает все данные
     * формы, однако в производных классах можно перекрыть метод и данные
     * отфильтровать и/или преобразовать.
     *
     * @return  array
     */
    protected function _data() 
    {
        return $this->_getValues();
    }

    /**
     * Заполняет элементы формы, значениями полученными из запроса.
     *
     */
    private function _getValuesFromRequest()
    {
        $data = $this->_getRequestData();

        foreach ($this->_elements as $name => $params) {
            // пропускаем поля только для чтения и скрытые
            if ($params['readonly'] || !$params['display']) {
                continue;
            }

            $index = $this->_getElementIndex($name);

            // :TODO: распихать по классам элементов
            switch ($params['type'])
            {
                case 'checkbox':
                    eval('$value = isset($data' . $index . ');');
                    break;

                case 'file':
                    $old = $this->_getValue($name);
                    $new = new Kernel_Form_Value_File($name);
                    if (!$new->isBlank()) {
                        $old->destruct();
                        $value = $new;
                    } else {
                        $value = $old;
                    }
                    break;

                default:
                    eval('$value = isset($data' . $index . ') ? $data' . $index . ' : null;');
                    break;
            }

            switch ($params['type'])
            {
                case 'date':
                    $value = tmp_get_date($value, $params['format']);
                    break;

                case 'time':
                    $value = tmp_get_time($value);
                    break;
            }

            if (is_string($value) && empty($params['no-trim']))
            {
                $value = trim($value);
            }

            $this->_elements[$name]['value'] = $value;
        }
    }

    /**
     * Возвращает строковое представление индекса элемента с указанным
     * наименованием в общем массиве значений элементов формы.
     *
     * @param   string  $name
     * @return  string
     */
    private function _getElementIndex($name) 
    {

        // оборачиваем имя поля в квадратные скобки
        $index = preg_replace('/^([^[]+)/', '[$1]', $name);

        // оборачиваем полученные индексы поля в кавычки
        return preg_replace('/\[([^]]+)\]/', '["$1"]', $index);
    }

    /**
     * Проверяет правильность ссылающейся страницы для того, чтобы форму нельзя
     * было отправить с другого хоста. Намеренно не игнорируем префикс "www."
     * т.к. все страницы должны быть в одном домене.
     *
     * @return  boolean
     */
    private function _isRefererValid()
    {
        return parse_url(Application::getRequest()->referer(), PHP_URL_HOST) == $_SERVER['HTTP_HOST'];
    }

    /**
     * Возвращает массив параметров успешно загруженных файлов.
     *
     * @param   string  $name
     * @return  boolean
     */
    protected function _getFiles($name = null) 
    {
        if (null !== $name) {
            $file = $this->_getElement($name);
            return 'file' == $file['type'] ? $file['value'] : false;
        }
        $files = array();
        foreach ($this->_getFileElements() as $name => $params) {
            if ($params['value']->successful()) {
                $files[$name] = $params['value'];
            }
        }
        return $files;
    }

    /**
     * Проверяет, наебнулись ли данные POST (когда загружен слишком большой
     * файл или просто отправлено слишком много данных).
     *
     * @return boolean
     */
    private function _isPostFailed() 
    {
        return empty($_FILES) && empty($_POST)
            && $_SERVER['CONTENT_LENGTH'] > ini_get_bytes('post_max_size');
    }

    /**
     * Возвращает данные об элементах формы для шаблона. Потом надо все
     * переделать на объекты. Пока это просто заглушка!
     *
     */
    private function _tmpGetElements()
    {
        $elements = array();
        
        foreach ($this->_elements as $name => $params)
        {
            if (!$params['display']) {
                continue;
            }

            if ('list' == $params['type']) {
                // инициализируем явно и заодно приводим к булевому типу
                $params['multiple'] = !empty($params['multiple']);

                if (isset($params['null']))
                {
                    $params['values'] = array('' => $params['null']) + $params['values'];
                    $params['null'] = true;
                }
                else
                {
                    $params['null'] = false;
                }
            }
            
            if ('position' == $params['type']) {
                $params['multiple'] = false;
                $params['null'] = false;
            }

            if ('file' == $params['type']) {
                $file = $params['value'];

                $params['value'] = $file->successful()
                    ? array(
                        'name' => $file->getName(),
                        'type' => $file->getType(),
                        'size' => $file->getSize(),
                    ) : null;
            }

            if (!isset($params['template'])) {
                $params['template'] = $params['type'];
            }
            
            $elements[$name] = $params;
        }
        
        return $elements;
    }

    private function _tmpIsMultiple($params)
    {
        return 'list' == $params['type'] && !empty($params['multiple']);
    }
}
